﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;

namespace Bouyei.GeoCore.GeoParser
{
    /// <summary>
    /// 摘自网上开源收集整理,*.dbf文件解析
    /// </summary>
    public class DbfParser:BaseFileParser
    {
        public DataTable dbfTable { get; private set; }
        private BinaryReader fileReader;
        private BinaryReader blockReader;
        private Encoding encoding = Encoding.ASCII;

        public override void Dispose()
        {
            if (dbfTable != null)
            {
                dbfTable.Clear();
                dbfTable.Dispose();
            }

            if (fileReader != null)
            {
                fileReader.Close();
                fileReader.Dispose();
                fileReader = null;
            }
            if (blockReader != null)
            {
                blockReader.Close();
                blockReader.Dispose();
                blockReader = null;
            }
        }

        public DbfParser(string dbfFile)
            :base(dbfFile)
        {
            encoding = Encoding.UTF8;
        }

        // Read an entire standard DBF file into a DataTable
        public DataTable FromReader()
        {
            Dispose();

            // Read the header into a buffer
            using (fileReader = new BinaryReader(File.OpenRead(filename)))
            {
                byte[] buffer = fileReader.ReadBytes(Marshal.SizeOf(typeof(DBFHeader)));

                // Marshall the header into a DBFHeader structure
                GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                DBFHeader header = (DBFHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(DBFHeader));
                handle.Free();

                // Read in all the field descriptors. Per the spec, 13 (0D) marks the end of the field descriptors
                List<FieldDescriptor> fields = new List<FieldDescriptor>();
     
                while ((13 != fileReader.PeekChar()))
                {
                    //fileReader.BaseStream.Position = fileReader.BaseStream.Position - 1;

                    buffer = fileReader.ReadBytes(Marshal.SizeOf(typeof(FieldDescriptor)));
                    handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                    fields.Add((FieldDescriptor)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(FieldDescriptor)));
                    handle.Free();
                }

                // Read in the first row of records, we need this to help determine column types below
                fileReader.BaseStream.Seek(header.headerLen + 1, SeekOrigin.Begin);
                buffer = fileReader.ReadBytes(header.recordLen);

                using (blockReader = new BinaryReader(new MemoryStream(buffer)))
                {
                    //fill  columns to table
                    ToColumnTable(blockReader, fields);

                    // Skip past the end of the header.
                    fileReader.BaseStream.Seek(header.headerLen, SeekOrigin.Begin);

                    //fill rows to table
                    ToRowsTable(header, fileReader, fields);
                }
            }

            return dbfTable;
        }

        public override string ToString()
        {
            if (dbfTable == null)
            {
                FromReader();
            }

            StringBuilder builder = new StringBuilder();

            int i = 0;
            for (; i < dbfTable.Columns.Count-1; ++i)
            {
                builder.AppendFormat("{0},", dbfTable.Columns[i].ColumnName);
            }
            builder.AppendFormat("{0}"+Environment.NewLine, dbfTable.Columns[i].ColumnName);

            foreach(DataRow row in dbfTable.Rows)
            {
                builder.AppendFormat("{0}" + Environment.NewLine, string.Join(",", row.ItemArray));
            }
            return builder.ToString();
        }

        private void ToColumnTable(BinaryReader reader, List<FieldDescriptor> fields)
        {
            // Create the columns in our new DataTable
            DataColumn col = null;
            dbfTable = new DataTable();

            foreach (FieldDescriptor field in fields)
            {
                switch (field.fieldType)
                {
                    case 'N':
                        byte[] b = blockReader.ReadBytes(field.fieldLen);
                        if (IsInter(b))
                        {
                            col = new DataColumn(field.fieldName, typeof(decimal));
                        }
                        else
                        {
                            col = new DataColumn(field.fieldName, typeof(int));
                        }
                        break;
                    case 'C':
                        col = new DataColumn(field.fieldName, typeof(string));
                        break;
                    case 'T':
                        // You can uncomment this to see the time component in the grid
                        //col = new DataColumn(field.fieldName, typeof(string));
                        col = new DataColumn(field.fieldName, typeof(DateTime));
                        break;
                    case 'D':
                        col = new DataColumn(field.fieldName, typeof(DateTime));
                        break;
                    case 'L':
                        col = new DataColumn(field.fieldName, typeof(bool));
                        break;
                    case 'F':
                        col = new DataColumn(field.fieldName, typeof(Double));
                        break;
                    default:
                        col = new DataColumn(field.fieldName, typeof(string));
                        break;
                }
                if (dbfTable.Columns.Contains(col.ColumnName) == false)
                    dbfTable.Columns.Add(col);
            }
        }

        private void ToRowsTable(DBFHeader header, BinaryReader fileReader, List<FieldDescriptor> fields)
        {
            byte[] buffer = null;
            DataRow row = null;
            int fieldIndex;
            string year;
            string month;
            string day;
            long lDate;
            long lTime;

            // Read in all the records
            for (int counter = 0; counter <= header.numRecords - 1; counter++)
            {
                // First we'll read the entire record into a buffer and then read each field from the buffer
                // This helps account for any extra space at the end of each record and probably performs better
                buffer = fileReader.ReadBytes(header.recordLen);

                using (blockReader = new BinaryReader(new MemoryStream(buffer)))
                {
                    if (blockReader.ReadChar() == '*')
                    {
                        continue;
                    }

                    fieldIndex = 0;
                    row = dbfTable.NewRow();

                    foreach (FieldDescriptor field in fields)
                    {
                        switch (field.fieldType)
                        {
                            case 'N':// Number
                                {
                                    byte[] b = blockReader.ReadBytes(field.fieldLen);
                                    if (IsInter(b))
                                    {
                                        row[fieldIndex] = int.Parse(encoding.GetString(b));
                                    }
                                    else
                                    {
                                        row[fieldIndex] = decimal.Parse(encoding.GetString(b));
                                    }
                                }
                                break;

                            case 'C': // String
                                {
                                    row[fieldIndex] = encoding.GetString(blockReader.ReadBytes(field.fieldLen)).Trim();
                                }
                                break;

                            case 'D': // Date (YYYYMMDD)
                                {
                                    year = encoding.GetString(blockReader.ReadBytes(4));
                                    month = encoding.GetString(blockReader.ReadBytes(2));
                                    day = encoding.GetString(blockReader.ReadBytes(2));
                                    row[fieldIndex] = System.DBNull.Value;
                                    try
                                    {
                                        row[fieldIndex] = new DateTime(Int32.Parse(year), Int32.Parse(month), Int32.Parse(day));
                                    }
                                    catch
                                    { }
                                }
                                break;

                            case 'T': // Timestamp, 8 bytes - two integers, first for date, second for time
                                      // Date is the number of days since 01/01/4713 BC (Julian Days)
                                      // Time is hours * 3600000L + minutes * 60000L + Seconds * 1000L (Milliseconds since midnight)
                                {
                                    lDate = blockReader.ReadInt32();
                                    lTime = blockReader.ReadInt32() * 10000L;
                                    row[fieldIndex] = JulianToDateTime(lDate).AddTicks(lTime);
                                }
                                break;
                            case 'L': // Boolean (Y/N)
                                if ('Y' == blockReader.ReadByte())
                                {
                                    row[fieldIndex] = true;
                                }
                                else
                                {
                                    row[fieldIndex] = false;
                                }
                                break;
                            case 'F':
                                {
                                    byte[] bf = blockReader.ReadBytes(field.fieldLen);

                                    if (IsInter(bf))
                                    {
                                        row[fieldIndex] = int.Parse(encoding.GetString(bf));
                                    }
                                    else
                                    {
                                        row[fieldIndex] = double.Parse(encoding.GetString(bf));
                                    }
                                }
                                break;
                        }
                        fieldIndex++;
                    }

                    blockReader.Close();
                }
                dbfTable.Rows.Add(row);
            }
        }
 
        private bool IsNumber(byte[] values)
        {
            unsafe
            {
                int number_count = 0;
                int point_count = 0;
                int space_count = 0;

                fixed (byte* s = values)
                {
                    for (int i = 0; i < values.Length; ++i)
                    {
                        byte b = *(s + i);
                        if ((b >= 48 && b <= 57))
                        {
                            number_count += 1;
                        }
                        else if (b == 46)
                        {
                            point_count += 1;
                        }
                        else if (b == 32)
                        {
                            space_count += 1;
                        }
                        else
                        {
                            if (!(b == 101 && (b + 1) == 43))//科学计数法问题
                            {
                                return false;
                            }
                        }
                    }
                }
                return (number_count > 0 && point_count < 2);
            }
        }

        private bool IsInter(byte[] values)
        {
            unsafe
            {
                fixed (byte* s = values)
                    for (int i = 0; i < values.Length; ++i)
                    {
                        byte b = *(s + i);

                        if (b == 46)
                        {
                            return false;
                        }
                    }
            }
            return false;
        }

        private DateTime JulianToDateTime(long lJDN)
        {
            double p = Convert.ToDouble(lJDN);
            double s1 = p + 68569;
            double n = Math.Floor(4 * s1 / 146097);
            double s2 = s1 - Math.Floor((146097 * n + 3) / 4);
            double i = Math.Floor(4000 * (s2 + 1) / 1461001);
            double s3 = s2 - Math.Floor(1461 * i / 4) + 31;
            double q = Math.Floor(80 * s3 / 2447);
            double d = s3 - Math.Floor(2447 * q / 80);
            double s4 = Math.Floor(q / 11);
            double m = q + 2 - 12 * s4;
            double j = 100 * (n - 49) + i + s4;
            return new DateTime(Convert.ToInt32(j), Convert.ToInt32(m), Convert.ToInt32(d));
        }
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    internal struct DBFHeader
    {
        public byte version;
        public byte updateYear;
        public byte updateMonth;
        public byte updateDay;
        public Int32 numRecords;
        public Int16 headerLen;
        public Int16 recordLen;
        public Int16 reserved1;
        public byte incompleteTrans;
        public byte encryptionFlag;
        public Int32 reserved2;
        public Int64 reserved3;
        public byte MDX;
        public byte language;
        public Int16 reserved4;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    internal struct FieldDescriptor
    {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 11)]
        public string fieldName;
        public char fieldType;
        public Int32 address;
        public byte fieldLen;
        public byte count;
        public Int16 reserved1;
        public byte workArea;
        public Int16 reserved2;
        public byte flag;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)]
        public byte[] reserved3;
        public byte indexFlag;
    }
}
